/**
 * Copyright (C) 2023 maminjie <canpool@163.com>
 * Copyright (C) 2017 Uwe Kindler
 * SPDX-License-Identifier: LGPL-2.1
 **/

#include "QxDockWidgetTab.h"
#include "QxDockAreaWidget.h"
#include "QxDockFocusController.h"
#include "QxDockManager.h"
#include "QxDockOverlay.h"
#include "QxDockWidget.h"
#include "QxDockElidingLabel.h"
#include "QxDockFloatingContainer.h"
#include "QxDockFloatingDragPreview.h"
#include "QxDockIconProvider.h"

#include <QxDockAutoHideContainer.h>
#include <QApplication>
#include <QBoxLayout>
#include <QDebug>
#include <QLabel>
#include <QMenu>
#include <QMouseEvent>
#include <QPushButton>
#include <QSplitter>
#include <QStyle>
#include <QToolButton>

QX_BEGIN_NAMESPACE

static const char *const LocationProperty = "Location";
using tTabLabel = DockElidingLabel;

/**
 * Private data class of DockWidgetTab class (pimpl)
 */
struct DockWidgetTabPrivate {
    DockWidgetTab *_this;
    DockWidget *dockWidget;
    QLabel *IconLabel = nullptr;
    tTabLabel *TitleLabel;
    QPoint GlobalDragStartMousePosition;
    QPoint DragStartMousePosition;
    bool IsActiveTab = false;
    DockAreaWidget *dockArea = nullptr;
    eDragState DragState = DraggingInactive;
    IFloatingWidget *FloatingWidget = nullptr;
    QIcon Icon;
    QAbstractButton *CloseButton = nullptr;
    QSpacerItem *IconTextSpacer;
    QPoint TabDragStartPosition;
    QSize IconSize;

    /**
     * Private data constructor
     */
    DockWidgetTabPrivate(DockWidgetTab *_public);

    /**
     * Creates the complete layout including all controls
     */
    void createLayout();

    /**
     * Moves the tab depending on the position in the given mouse event
     */
    void moveTab(QMouseEvent *ev);

    /**
     * Test function for current drag state
     */
    bool isDraggingState(eDragState dragState) const
    {
        return this->DragState == dragState;
    }

    /**
     * Starts floating of the dock widget that belongs to this title bar
     * Returns true, if floating has been started and false if floating
     * is not possible for any reason
     */
    bool startFloating(eDragState DraggingState = DraggingFloatingWidget);

    /**
     * Returns true if the given config flag is set
     */
    bool testConfigFlag(DockManager::eConfigFlag Flag) const
    {
        return DockManager::testConfigFlag(Flag);
    }

    /**
     * Creates the close button as QPushButton or as QToolButton
     */
    QAbstractButton *createCloseButton() const
    {
        if (testConfigFlag(DockManager::TabCloseButtonIsToolButton)) {
            auto Button = new QToolButton();
            Button->setAutoRaise(true);
            return Button;
        } else {
            return new QPushButton();
        }
    }

    /**
     * Update the close button visibility from current feature/config
     */
    void updateCloseButtonVisibility(bool active)
    {
        bool DockWidgetClosable = dockWidget->features().testFlag(DockWidget::DockWidgetClosable);
        bool ActiveTabHasCloseButton = testConfigFlag(DockManager::ActiveTabHasCloseButton);
        bool AllTabsHaveCloseButton = testConfigFlag(DockManager::AllTabsHaveCloseButton);
        bool TabHasCloseButton = (ActiveTabHasCloseButton && active) | AllTabsHaveCloseButton;
        CloseButton->setVisible(DockWidgetClosable && TabHasCloseButton);
    }

    /**
     * Update the size policy of the close button depending on the
     * RetainTabSizeWhenCloseButtonHidden feature
     */
    void updateCloseButtonSizePolicy()
    {
        auto Features = dockWidget->features();
        auto SizePolicy = CloseButton->sizePolicy();
        SizePolicy.setRetainSizeWhenHidden(Features.testFlag(DockWidget::DockWidgetClosable) &&
                                           testConfigFlag(DockManager::RetainTabSizeWhenCloseButtonHidden));
        CloseButton->setSizePolicy(SizePolicy);
    }

    template <typename T> IFloatingWidget *createFloatingWidget(T *Widget, bool CreateContainer)
    {
        if (CreateContainer) {
            return new DockFloatingContainer(Widget);
        } else {
            auto w = new DockFloatingDragPreview(Widget);
            _this->connect(w, &DockFloatingDragPreview::draggingCanceled, [=]() {
                DragState = DraggingInactive;
            });
            return w;
        }
    }

    /**
     * Saves the drag start position in global and local coordinates
     */
    void saveDragStartMousePosition(const QPoint &GlobalPos)
    {
        GlobalDragStartMousePosition = GlobalPos;
        DragStartMousePosition = _this->mapFromGlobal(GlobalPos);
    }

    /**
     * Update the icon in case the icon size changed
     */
    void updateIcon()
    {
        if (!IconLabel || Icon.isNull()) {
            return;
        }

        if (IconSize.isValid()) {
            IconLabel->setPixmap(Icon.pixmap(IconSize));
        } else {
            IconLabel->setPixmap(Icon.pixmap(_this->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, _this)));
        }
        IconLabel->setVisible(true);
    }

    /**
     * Convenience function for access to the dock manager dock focus controller
     */
    DockFocusController *focusController() const
    {
        return dockWidget->dockManager()->dockFocusController();
    }

    /**
     * Helper function to create and initialize the menu entries for
     * the "Auto Hide Group To..." menu
     */
    QAction *createAutoHideToAction(const QString &Title, SideBarLocation Location, QMenu *Menu)
    {
        auto Action = Menu->addAction(Title);
        Action->setProperty("Location", Location);
        QObject::connect(Action, &QAction::triggered, _this, &DockWidgetTab::onAutoHideToActionClicked);
        return Action;
    }
};
// struct DockWidgetTabPrivate

DockWidgetTabPrivate::DockWidgetTabPrivate(DockWidgetTab *_public) : _this(_public)
{
}

void DockWidgetTabPrivate::createLayout()
{
    TitleLabel = new tTabLabel();
    TitleLabel->setElideMode(Qt::ElideRight);
    TitleLabel->setText(dockWidget->windowTitle());
    TitleLabel->setObjectName("dockWidgetTabLabel");
    TitleLabel->setAlignment(Qt::AlignCenter);
    _this->connect(TitleLabel, SIGNAL(elidedChanged(bool)), SIGNAL(elidedChanged(bool)));

    CloseButton = createCloseButton();
    CloseButton->setObjectName("tabCloseButton");
    internal::setButtonIcon(CloseButton, QStyle::SP_TitleBarCloseButton, TabCloseIcon);
    CloseButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    CloseButton->setFocusPolicy(Qt::NoFocus);
    updateCloseButtonSizePolicy();
    internal::setToolTip(CloseButton, QObject::tr("Close Tab"));
    _this->connect(CloseButton, SIGNAL(clicked()), SIGNAL(closeRequested()));

    QFontMetrics fm(TitleLabel->font());
    int Spacing = qRound(fm.height() / 4.0);

    // Fill the layout
    QBoxLayout *Layout = new QBoxLayout(QBoxLayout::LeftToRight);
    Layout->setContentsMargins(2 * Spacing, 0, 0, 0);
    Layout->setSpacing(0);
    _this->setLayout(Layout);
    Layout->addWidget(TitleLabel, 1);
    Layout->addSpacing(Spacing);
    Layout->addWidget(CloseButton);
    Layout->addSpacing(qRound(Spacing * 4.0 / 3.0));
    Layout->setAlignment(Qt::AlignCenter);

    TitleLabel->setVisible(true);
}

void DockWidgetTabPrivate::moveTab(QMouseEvent *ev)
{
    ev->accept();
    QPoint Distance = internal::globalPositionOf(ev) - GlobalDragStartMousePosition;
    Distance.setY(0);
    auto TargetPos = Distance + TabDragStartPosition;
    TargetPos.rx() = qMax(TargetPos.x(), 0);
    TargetPos.rx() = qMin(_this->parentWidget()->rect().right() - _this->width() + 1, TargetPos.rx());
    _this->move(TargetPos);
    _this->raise();
}

bool DockWidgetTabPrivate::startFloating(eDragState DraggingState)
{
    auto dockContainer = dockWidget->dockContainer();
    QX_DOCK_PRINT("isFloating " << dockContainer->isFloating());
    QX_DOCK_PRINT("areaCount " << dockContainer->dockAreaCount());
    QX_DOCK_PRINT("widgetCount " << dockWidget->dockAreaWidget()->dockWidgetsCount());
    // if this is the last dock widget inside of this floating widget,
    // then it does not make any sense, to make it floating because
    // it is already floating
    if (dockContainer->isFloating() && (dockContainer->visibleDockAreaCount() == 1) &&
        (dockWidget->dockAreaWidget()->dockWidgetsCount() == 1)) {
        return false;
    }

    QX_DOCK_PRINT("startFloating");
    DragState = DraggingState;
    IFloatingWidget *FloatingWidget = nullptr;
    bool CreateContainer = (DraggingFloatingWidget != DraggingState);

    // If section widget has multiple tabs, we take only one tab
    // If it has only one single tab, we can move the complete
    // dock area into floating widget
    QSize Size;
    if (dockArea->dockWidgetsCount() > 1) {
        FloatingWidget = createFloatingWidget(dockWidget, CreateContainer);
        Size = dockWidget->size();
    } else {
        FloatingWidget = createFloatingWidget(dockArea, CreateContainer);
        Size = dockArea->size();
    }

    if (DraggingFloatingWidget == DraggingState) {
        FloatingWidget->startFloating(DragStartMousePosition, Size, DraggingFloatingWidget, _this);
        auto dockManager = dockWidget->dockManager();
        auto Overlay = dockManager->containerOverlay();
        Overlay->setAllowedAreas(OuterDockAreas);
        this->FloatingWidget = FloatingWidget;
        qApp->postEvent(dockWidget, new QEvent((QEvent::Type)internal::DockedWidgetDragStartEvent));
    } else {
        FloatingWidget->startFloating(DragStartMousePosition, Size, DraggingInactive, nullptr);
    }

    return true;
}

DockWidgetTab::DockWidgetTab(DockWidget *dockWidget, QWidget *parent)
    : QFrame(parent), d(new DockWidgetTabPrivate(this))
{
    setAttribute(Qt::WA_NoMousePropagation, true);
    d->dockWidget = dockWidget;
    d->createLayout();
    setFocusPolicy(Qt::NoFocus);
}

DockWidgetTab::~DockWidgetTab()
{
    QX_DOCK_PRINT("~DockWidgetTab()");
    delete d;
}

void DockWidgetTab::mousePressEvent(QMouseEvent *ev)
{
    if (ev->button() == Qt::LeftButton) {
        ev->accept();
        d->saveDragStartMousePosition(internal::globalPositionOf(ev));
        d->DragState = DraggingMousePressed;
        if (DockManager::testConfigFlag(DockManager::FocusHighlighting)) {
            d->focusController()->setDockWidgetTabFocused(this);
        }
        Q_EMIT clicked();
        return;
    }
    Super::mousePressEvent(ev);
}

void DockWidgetTab::mouseReleaseEvent(QMouseEvent *ev)
{
    if (ev->button() == Qt::LeftButton) {
        auto CurrentDragState = d->DragState;
        d->GlobalDragStartMousePosition = QPoint();
        d->DragStartMousePosition = QPoint();
        d->DragState = DraggingInactive;

        switch (CurrentDragState) {
        case DraggingTab:
            // End of tab moving, emit signal
            if (d->dockArea) {
                ev->accept();
                Q_EMIT moved(internal::globalPositionOf(ev));
            }
            break;

        case DraggingFloatingWidget:
            ev->accept();
            d->FloatingWidget->finishDragging();
            break;

        default:;   // do nothing
        }
    } else if (ev->button() == Qt::MiddleButton) {
        if (DockManager::testConfigFlag(DockManager::MiddleMouseButtonClosesTab) &&
            d->dockWidget->features().testFlag(DockWidget::DockWidgetClosable)) {
            // Only attempt to close if the mouse is still
            // on top of the widget, to allow the user to cancel.
            if (rect().contains(mapFromGlobal(QCursor::pos()))) {
                ev->accept();
                Q_EMIT closeRequested();
            }
        }
    }

    Super::mouseReleaseEvent(ev);
}

void DockWidgetTab::mouseMoveEvent(QMouseEvent *ev)
{
    if (!(ev->buttons() & Qt::LeftButton) || d->isDraggingState(DraggingInactive)) {
        d->DragState = DraggingInactive;
        Super::mouseMoveEvent(ev);
        return;
    }

    // move floating window
    if (d->isDraggingState(DraggingFloatingWidget)) {
        d->FloatingWidget->moveFloating();
        Super::mouseMoveEvent(ev);
        return;
    }

    // move tab
    if (d->isDraggingState(DraggingTab)) {
        // Moving the tab is always allowed because it does not mean moving the
        // dock widget around
        d->moveTab(ev);
    }

    auto MappedPos = mapToParent(ev->pos());
    bool MouseOutsideBar = (MappedPos.x() < 0) || (MappedPos.x() > parentWidget()->rect().right());
    // Maybe a fixed drag distance is better here ?
    int DragDistanceY = qAbs(d->GlobalDragStartMousePosition.y() - internal::globalPositionOf(ev).y());
    if (DragDistanceY >= DockManager::startDragDistance() || MouseOutsideBar) {
        // If this is the last dock area in a dock container with only
        // one single dock widget it does not make  sense to move it to a new
        // floating widget and leave this one empty
        if (d->dockArea->dockContainer()->isFloating() && d->dockArea->openDockWidgetsCount() == 1 &&
            d->dockArea->dockContainer()->visibleDockAreaCount() == 1) {
            return;
        }

        // Floating is only allowed for widgets that are floatable
        // We can create the drag preview if the widget is movable.
        auto Features = d->dockWidget->features();
        if (Features.testFlag(DockWidget::DockWidgetFloatable) ||
            (Features.testFlag(DockWidget::DockWidgetMovable))) {
            // If we undock, we need to restore the initial position of this
            // tab because it looks strange if it remains on its dragged position
            if (d->isDraggingState(DraggingTab)) {
                parentWidget()->layout()->update();
            }
            d->startFloating();
        }
        return;
    } else if (d->dockArea->openDockWidgetsCount() > 1 &&
               (internal::globalPositionOf(ev) - d->GlobalDragStartMousePosition).manhattanLength() >=
                   QApplication::startDragDistance())   // Wait a few pixels before start moving
    {
        // If we start dragging the tab, we save its inital position to
        // restore it later
        if (DraggingTab != d->DragState) {
            d->TabDragStartPosition = this->pos();
        }
        d->DragState = DraggingTab;
        return;
    }

    Super::mouseMoveEvent(ev);
}

void DockWidgetTab::contextMenuEvent(QContextMenuEvent *ev)
{
    ev->accept();
    if (d->isDraggingState(DraggingFloatingWidget)) {
        return;
    }

    d->saveDragStartMousePosition(ev->globalPos());

    const bool isFloatable = d->dockWidget->features().testFlag(DockWidget::DockWidgetFloatable);
    const bool isNotOnlyTabInContainer = !d->dockArea->dockContainer()->hasTopLevelDockWidget();
    const bool isTopLevelArea = d->dockArea->isTopLevelArea();
    const bool isDetachable = isFloatable && isNotOnlyTabInContainer;
    QAction *Action;
    QMenu Menu(this);

    if (!isTopLevelArea) {
        Action = Menu.addAction(tr("Detach"), this, SLOT(detachDockWidget()));
        Action->setEnabled(isDetachable);
        if (DockManager::testAutoHideConfigFlag(DockManager::AutoHideFeatureEnabled)) {
            Action = Menu.addAction(tr("Pin"), this, SLOT(autoHideDockWidget()));
            auto IsPinnable = d->dockWidget->features().testFlag(DockWidget::DockWidgetPinnable);
            Action->setEnabled(IsPinnable);

            auto menu = Menu.addMenu(tr("Pin To..."));
            menu->setEnabled(IsPinnable);
            d->createAutoHideToAction(tr("Top"), SideBarTop, menu);
            d->createAutoHideToAction(tr("Left"), SideBarLeft, menu);
            d->createAutoHideToAction(tr("Right"), SideBarRight, menu);
            d->createAutoHideToAction(tr("Bottom"), SideBarBottom, menu);
        }
    }

    Menu.addSeparator();
    Action = Menu.addAction(tr("Close"), this, SIGNAL(closeRequested()));
    Action->setEnabled(isClosable());
    if (d->dockArea->openDockWidgetsCount() > 1) {
        Action = Menu.addAction(tr("Close Others"), this, SIGNAL(closeOtherTabsRequested()));
    }
    Menu.exec(ev->globalPos());
}

bool DockWidgetTab::isActiveTab() const
{
    return d->IsActiveTab;
}

void DockWidgetTab::setActiveTab(bool active)
{
    d->updateCloseButtonVisibility(active);

    // Focus related stuff
    if (DockManager::testConfigFlag(DockManager::FocusHighlighting) &&
        !d->dockWidget->dockManager()->isRestoringState()) {
        bool UpdateFocusStyle = false;
        if (active && !hasFocus()) {
            // setFocus(Qt::OtherFocusReason);
            d->focusController()->setDockWidgetTabFocused(this);
            UpdateFocusStyle = true;
        }

        if (d->IsActiveTab == active) {
            if (UpdateFocusStyle) {
                updateStyle();
            }
            return;
        }
    } else if (d->IsActiveTab == active) {
        return;
    }

    d->IsActiveTab = active;
    updateStyle();
    update();
    updateGeometry();

    Q_EMIT activeTabChanged();
}

DockWidget *DockWidgetTab::dockWidget() const
{
    return d->dockWidget;
}

void DockWidgetTab::setDockAreaWidget(DockAreaWidget *dockArea)
{
    d->dockArea = dockArea;
}

DockAreaWidget *DockWidgetTab::dockAreaWidget() const
{
    return d->dockArea;
}

void DockWidgetTab::setIcon(const QIcon &Icon)
{
    QBoxLayout *Layout = qobject_cast<QBoxLayout *>(layout());
    if (!d->IconLabel && Icon.isNull()) {
        return;
    }

    if (!d->IconLabel) {
        d->IconLabel = new QLabel();
        d->IconLabel->setAlignment(Qt::AlignVCenter);
        d->IconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
        internal::setToolTip(d->IconLabel, d->TitleLabel->toolTip());
        Layout->insertWidget(0, d->IconLabel, Qt::AlignVCenter);
        Layout->insertSpacing(1, qRound(1.5 * Layout->contentsMargins().left() / 2.0));
    } else if (Icon.isNull()) {
        // Remove icon label and spacer item
        Layout->removeWidget(d->IconLabel);
        Layout->removeItem(Layout->itemAt(0));
        delete d->IconLabel;
        d->IconLabel = nullptr;
    }

    d->Icon = Icon;
    d->updateIcon();
}

const QIcon &DockWidgetTab::icon() const
{
    return d->Icon;
}

QString DockWidgetTab::text() const
{
    return d->TitleLabel->text();
}

void DockWidgetTab::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        // If this is the last dock area in a dock container it does not make
        // sense to move it to a new floating widget and leave this one
        // empty
        if ((!d->dockArea->dockContainer()->isFloating() || d->dockArea->dockWidgetsCount() > 1) &&
            d->dockWidget->features().testFlag(DockWidget::DockWidgetFloatable)) {
            event->accept();
            d->saveDragStartMousePosition(internal::globalPositionOf(event));
            d->startFloating(DraggingInactive);
        }
    }

    Super::mouseDoubleClickEvent(event);
}

void DockWidgetTab::setVisible(bool visible)
{
    visible &= !d->dockWidget->features().testFlag(DockWidget::NoTab);
    Super::setVisible(visible);
}

void DockWidgetTab::setText(const QString &title)
{
    d->TitleLabel->setText(title);
}

bool DockWidgetTab::isTitleElided() const
{
    return d->TitleLabel->isElided();
}

bool DockWidgetTab::isClosable() const
{
    return d->dockWidget && d->dockWidget->features().testFlag(DockWidget::DockWidgetClosable);
}

void DockWidgetTab::detachDockWidget()
{
    if (!d->dockWidget->features().testFlag(DockWidget::DockWidgetFloatable)) {
        return;
    }

    d->saveDragStartMousePosition(QCursor::pos());
    d->startFloating(DraggingInactive);
}

void DockWidgetTab::autoHideDockWidget()
{
    d->dockWidget->setAutoHide(true);
}

void DockWidgetTab::onAutoHideToActionClicked()
{
    int Location = sender()->property(LocationProperty).toInt();
    d->dockWidget->toggleAutoHide((SideBarLocation)Location);
}

bool DockWidgetTab::event(QEvent *e)
{
#ifndef QT_NO_TOOLTIP
    if (e->type() == QEvent::ToolTipChange) {
        const auto text = toolTip();
        d->TitleLabel->setToolTip(text);
        if (d->IconLabel) {
            d->IconLabel->setToolTip(text);
        }
    }
#endif
    if (e->type() == QEvent::StyleChange) {
        d->updateIcon();
    }
    return Super::event(e);
}

void DockWidgetTab::onDockWidgetFeaturesChanged()
{
    d->updateCloseButtonSizePolicy();
    d->updateCloseButtonVisibility(isActiveTab());
}

void DockWidgetTab::setElideMode(Qt::TextElideMode mode)
{
    d->TitleLabel->setElideMode(mode);
}

void DockWidgetTab::updateStyle()
{
    internal::repolishStyle(this, internal::RepolishDirectChildren);
}

QSize DockWidgetTab::iconSize() const
{
    return d->IconSize;
}

void DockWidgetTab::setIconSize(const QSize &Size)
{
    d->IconSize = Size;
    d->updateIcon();
}

QX_END_NAMESPACE
